
IsFunction = (object) -> !!(object && object.constructor && object.call && object.apply)
IsNotifyList = (object) -> object instanceof NotifyList
IsNotify = (obj) -> obj instanceof Notify
IsString = (o) -> o.substring != undefined

get_context_args = (incoming_context, next_context) ->
  stack = incoming_context.slice(0)
  stack.splice(0,0, next_context)
  return stack

AreArraysEqual = (lhs, rhs) ->
  if lhs is undefined or rhs is undefined then return false
  if rhs == null and lhs == null then return true
  if lhs == null or rhs == null then return false
  if lhs.length != rhs.length then return false
  for k, v of lhs
    if rhs[k] != v then return false
  return true

class Notify
  _notify_instance_count_ = 0
  constructor : (InitialValue = null) ->
    @id = "_NOTIFY_OBJ_#{_notify_instance_count_++}"
    @Value = InitialValue
    @Handlers = []

  GetValue : () => return @Value
  SetValue : (value) =>
    old_value = @Value
    @Value = value

    for handler in @Handlers
      handler( value, old_value )
    return value

  Subscribe : (handler) ->
    @Handlers.push( handler )

  Unsubscribe : (handler) =>
    plength = @Handlers.length
    @Handlers = (h for h in @Handlers when h isnt handler )
    if plength == @Handlers.length
      throw new Error( "Failed to unsubscribe, unable to find matching handler" )

  DropAllSubscriptions : () ->
    @Handlers = []

class NotifyArray extends Notify
  constructor : (initial_value = []) ->
    super( initial_value )

  add : ( item ) ->
    array = @Value.slice(0)
    array.push( item )
    @SetValue( array )

  remove : (item) ->
    array = (o for o in @Value when o isnt item)
    @SetValue( array )

  pop : () ->
    array = @value.slice(0)
    item = array.pop()
    @SetValue( array )
    return item

  clear : () ->
    @SetValue( [] )

  reset : (array) ->
    @SetValue( array )



####################################################################################
## NotifyObj - a dictionary that makes calls when attributes are set.
####################################################################################
# CONSTRUCTOR ARG TAKES OBJ:
# {
#   one : (name, n_value, o_value ) - setter notification
#   many : (new_dict, old_dict ) - setter notification
#   input_conversion : (name, value) -> converted value. The converted value gets stored.
# }
# any or all of the above can be null.
#
# It's important to note that thousands of these may be created. So we keep it lightweight by avoiding allocations.
class NotifyObj
  constructor : ( arg = {} ) ->
    @pvt = new NotifyObjImpl( arg.one, arg.many, arg.input_conversion, arg.remove, {} )
  get : (name) -> @pvt.values[name]
  set : (name_or_dict, value) ->
    if IsString( name_or_dict )
      @pvt.do_one( name_or_dict, value )
    else
      @pvt.do_many( name_or_dict )
  reapply : () ->
    for k, v of @pvt.values
      @pvt.one( k, v, null )
  values : () ->
    return @pvt.values

  # actual implementation is here.
  class NotifyObjImpl
    constructor : ( one, many, input_conversion, remove, values ) ->
      # explicitly don't assign anything unless its there
      if one then @one = one
      if many then @many = many
      if input_conversion then @input_conversion = input_conversion
      if remove then @remove = remove
      @values = values

    do_element : ( name, value ) =>
      if @input_conversion and value
        value = @input_conversion( name, value )
      o = @values[name]
      n = @values[name] = value

      if value is undefined or value is null
        delete @values[name]

      if @remove and !n and o
        @remove( name, o )

      return [name, n, o]

    do_one : (name, value) =>
      result = @do_element( name, value )
      if @one
        @one( result... )

    do_many : (obj) =>
      o = {}
      n = {}
      for k, v of obj
        [name, c, p] = @do_element( k, v )
        o[name] = p
        n[name] = c
      if @many
        @many( n, o )
      else if @one
        for k, v of n
          @one( k, v, o[k])

####################################################################################
## ExprArgument - handles dealing with arguments that can be functions
##                that can potentially return Notifys
####################################################################################

_expr_get_type = (expr) ->
  if not expr? then throw new Error( "undefined or null expr")
  if IsString( expr ) then return "StringLiteral"
  else if IsNotify(expr) then return "Notify"
  else if IsFunction(expr) then return "Selector"
  else return "StaticObject"

_expr_wrappers =
  StringLiteral : (expr) -> (context) -> return expr
  StaticObject : (expr) -> (context) -> return expr
  Notify : (expr) -> (context) -> return expr.GetValue()
  Selector : (expr) -> return expr

# ARGS:
#   setter: (new, old)
#   expr: some valid expression
ExprArgument = ( expr ) ->
  type = _expr_get_type( expr )
  locals =
    expr : _expr_wrappers[ type ]( expr )
    setter : []
    intermediate_notify : null
    value : null
    context : undefined
    change_handler : (c, p, check_eq = true) =>
      if @is_released
        throw new Error("change handler released - but firing")
      if check_eq and c is p
        return c
      locals.value = c
      for handler in locals.setter
        handler( c, p )
      return c
  @setter = (setter) ->
    if setter.length != 2
      throw new Error("setter requires 2 arguments, (cValue, pValue)")
    locals.setter.push( setter )
    return this

  #  @reapply = () ->
  #    if locals.context != undefined
  #      locals.change_handler( locals.value, locals.value, false )
  #
  #  @GetExpr = () -> return locals.expr

  @is_released = false
  @release = () ->
    if @is_released
      throw new Error("released twice")
    @is_released = true
    if locals.intermediate_notify?
      locals.intermediate_notify.Unsubscribe( locals.change_handler )

  @SetContext = (context...) =>
    if @is_released
      throw new Error("released")
    # after changing context to splat, i don't think this will ever work correctly - need to check each element
    if AreArraysEqual(locals.context, context)
      return context

    # TODO: this is probably not needed anymore.
    if context[0] is null
      throw new Error("just checking if this ever happens at this point")
      old = locals.value
      locals.value = null
      locals.context = null
      if locals.intermediate_notify?
        locals.intermediate_notify.Unsubscribe( locals.change_handler )
        locals.intermediate_notify = null
      locals.change_handler( null, old )
      return null

    locals.context = context
    nvalue = locals.expr( context... )

    # is there a change? if not, return
    if nvalue is locals.value
      return locals.value

    # still not really a change, since nvalue is the notify we dealt with last time
    if nvalue is locals.intermediate_notify
      return nvalue.GetValue()

    # at this point we're assured a value is changing, so unsubscribe and clear intermediate
    if locals.intermediate_notify?
      locals.intermediate_notify.Unsubscribe( locals.change_handler )
      locals.intermediate_notify = null

    # our new value is a notify, so subscribe, assign it to locals, and get the actual value out
    if IsNotify( nvalue )
      locals.intermediate_notify = nvalue
      locals.intermediate_notify.Subscribe( locals.change_handler )
      nvalue = nvalue.GetValue()

    return locals.change_handler( nvalue, locals.value )
  return this

get_context_args = (incoming_context, next_context) ->
  stack = incoming_context.slice(0)
  stack.splice(0,0, next_context)
  return stack

####################################################################################
## TagInstance - essentially a wrapper for a dom element, passes data to the element
####################################################################################
tag_instance_count = 0

class TagEnterExit
  constructor : ( tag_declaration ) ->
    @tag_declaration = tag_declaration

  attr : (name_or_dict, value = undefined) ->
    if @attributes is undefined then @attributes = new NotifyObj()
    @attributes.set( name_or_dict, value )
    return @tag_declaration

  style : (name_or_dict, value = undefined ) ->
    if @styles is undefined then @styles = new NotifyObj()
    @styles.set( name_or_dict, value )
    return @tag_declaration

class TagDeclaration
  constructor : ( tag, children...) ->
    @tag = tag
    @children = children

  append : (children...) ->
    @children = @children.concat( children )
    return this

  text : (expr) ->
    @text_expr = expr
    return this

  enter_attr : (name_or_dict, value = undefined) ->
    if @enter_configuration is undefined
      @enter_configuration = new TagEnterExit( this )
    @enter_configuration.attr(name_or_dict, value)
    return this
  exit_attr : (name_or_dict, value = undefined) ->
    if @exit_configuration is undefined
      @exit_configuration = new TagEnterExit( this )
    @exit_configuration.attr(name_or_dict, value)
    return this
  enter_style : (name_or_dict, value = undefined) ->
    if @enter_configuration is undefined
      @enter_configuration = new TagEnterExit( this )
    @enter_configuration.style(name_or_dict, value)
    return this
  exit_style : (name_or_dict, value = undefined) ->
    if @exit_configuration is undefined
      @exit_configuration = new TagEnterExit( this )
    @exit_configuration.style(name_or_dict, value)
    return this


  attr : (name_or_dict, value = undefined ) ->
    if @attributes is undefined then @attributes = new NotifyObj()
    @attributes.set( name_or_dict, value )
    return this

  style : (name_or_dict, value = undefined ) ->
    if @styles is undefined then @styles = new NotifyObj()
    @styles.set( name_or_dict, value )
    return this

  on : (name_or_dict, value = undefined ) ->
    if @events is undefined then @events = new NotifyObj()
    if value isnt undefined
      @events.set( name_or_dict, (() -> value ) )
    else
      wrapped_handlers = {}
      for k, v of name_or_dict
        wrapped_handlers[k] = () -> name_or_dict[k]
      @events.set( wrapped_handlers )

    return this

  event : (name_or_dict, value = undefined ) ->
    if @events is undefined then @events = new NotifyObj()
    @events.set( name_or_dict, value )
    return this


  write : ( parent_dom, context_stack... ) ->
    tag = new Tag( this, parent_dom, context_stack )
    children = []
    for child in @children
      c = child.write( tag.dom_element, context_stack... )
      children = children.concat( c )
    tag.append( children... )
    return [tag]

class Tag
  _build_style_ = (tag) ->
    style_tag = ""
    for k, v of tag.resolved_styles.values()
      style_tag = style_tag.concat( "#{k}:#{v};")
    if tag._pstyle_ != style_tag
      tag.attributes.set( "style", style_tag )
      tag._pstyle_ = style_tag


  constructor : ( tag_declaration, parent_dom, context_stack ) ->
    @dom_element = document.createElement( tag_declaration.tag )
    @parent_dom_element = parent_dom
    @tag_declaration = tag_declaration

    if tag_declaration.text_expr isnt undefined
      @text_expr = new ExprArgument( tag_declaration.text_expr ).setter((n,o) =>
        @dom_element.innerText = n
      )

    if tag_declaration.attributes isnt undefined or tag_declaration.styles isnt undefined or
       tag_declaration.enter_configuration

      @attributes = new NotifyObj(
        input_conversion : (name, expr) =>
          return new ExprArgument( expr ).setter( (n, o) =>
            if n?
              @dom_element.setAttribute( name, n )
          )
        remove : (name) =>
          @dom_element.removeAttribute( name )
      )


    if tag_declaration.styles isnt undefined or tag_declaration.enter_configuration?.styles != undefined
      # ok, how do styles work? When a style element changes, we need to update the styles tag.
      @_pstyle_ = ""
      @styles = new NotifyObj(
        input_conversion : (name, expr) =>
          new ExprArgument( expr ).setter(( n, o ) =>
            @resolved_styles.set( name, n )
          )
        remove : (name, expr) =>
          expr.release()
      )
      @resolved_styles = new NotifyObj(
        one : (k, n, o ) => _build_style_(this)
        many : (n, o) => _build_style_(this)
      )
    if tag_declaration.events isnt undefined
  #    if tag_declaration.events isnt undefined
      @primary_events = {}
      @events = new NotifyObj(
        input_conversion : (name, expr) =>
          new ExprArgument( expr ).setter( (n,o) =>
            if o?
              @dom_element.removeEventListener( name, @primary_events[name] )
              delete @primary_events[name]
            if n?
              primary_handler = @primary_events[name] = (native_args...) =>
                _CORE_._NativeEvent_ = native_args[0]
                n( @context_stack...)
              @dom_element.addEventListener( name, primary_handler )
          )
      )

    has_attrib = tag_declaration.attributes != undefined
    has_style = tag_declaration.styles != undefined
    has_update = ( has_attrib or has_style )
    has_enter = tag_declaration.enter_configuration != undefined
    has_enter_style = tag_declaration.enter_configuration?.styles != undefined
    has_enter_attrib = tag_declaration.enter_configuration?.attributes != undefined
    has_event = tag_declaration.events != undefined

    # if enter exists, always copy
    if has_enter
      if has_enter_attrib
        @attributes.set( tag_declaration.enter_configuration.attributes.values() )
      if has_enter_style
        @styles.set( tag_declaration.enter_configuration.styles.values())
    else
      if has_attrib
        @attributes.set( tag_declaration.attributes.values() )
      if has_style
        @styles.set( tag_declaration.styles.values() )

    if has_event
      @events.set( tag_declaration.events.values() )

    @context( context_stack... )
    parent_dom.appendChild( @dom_element )

    if has_enter and has_update
      setTimeout( (() => _apply_attributes_(this, tag_declaration, context_stack)), 0 )

  _apply_attributes_ = (tag, tag_declaration, context_stack)->
    has_attrib = tag_declaration.attributes != undefined
    has_style = tag_declaration.styles != undefined
    if has_attrib
      tag.attributes.set( tag_declaration.attributes.values() )
    if has_style
      tag.styles.set( tag_declaration.styles.values() )
    tag.context( context_stack... )


  append : (children...) =>
    if @children is undefined then @children = []
    for child in children
      @children.push( child )

  context_stack : undefined

  _all_ = ( notifyobjs, callback ) ->
    for notifyobj in notifyobjs
      if notifyobj is undefined then continue
      for name, expr of notifyobj.values()
        callback( expr )

  release : () =>
    computed_style = getComputedStyle(@dom_element, null)
    duration = computed_style.getPropertyValue("-webkit-transition-duration")
    if duration
      duration = duration.substring(0, duration.length-1) * 1000

      if duration > 0 and @tag_declaration.exit_configuration != undefined
        if @tag_declaration.exit_configuration.attributes != undefined
          @attributes.set( @tag_declaration.exit_configuration.attributes.values() )
        if @tag_declaration.exit_configuration.styles != undefined
          @styles.set( @tag_declaration.exit_configuration.styles.values() )
        @context( @context_stack... )

        setTimeout( @direct_release, duration  )
      else
        @direct_release()
    else
      @direct_release()

  direct_release: () =>
#    console.log( "RELEASING", @dom_element )
#    console.trace()
    @dom_element.remove()#@parent_dom_element.remove( @dom_element )

    if @text_expr isnt undefined then @text_expr.release()
    _all_([ @styles, @attributes, @events ], (expr) ->
      expr.release()
    )

    if @children isnt undefined
      for child in @children
        child.release()
    delete @children
    delete @dom_element
    delete @primary_events
    delete @tag_declaration

  inner_release : () => @direct_release()

  context : ( context_stack... ) =>
    @context_stack = context_stack
    if @text_expr isnt undefined then @text_expr.SetContext( context_stack... )
    # note that ordering here is actually important, since styles relies on attributes.
    # we should perhaps change this and just have style write directly to dom.
    _all_([ @styles, @attributes, @events ], (expr) ->
      expr.SetContext( context_stack... )
    )
    if @children isnt undefined
      for child in @children
        child.context( context_stack... )

class ContextDeclaration
  constructor : (@expr, @children... ) ->

  append : (children...) ->
    for child in children
      @children.append( child )

  write : ( parent_dom, context_stack... ) =>
    return new Context( @expr, parent_dom, @children, context_stack )

class Context
  constructor : ( expr, parent_dom, children, context_stack) ->
    @tags = []
    @children = children
    @parent_dom = parent_dom
    @context_stack = context_stack
    @expr_arg = new ExprArgument( expr ).setter( (n,o) =>
      if @is_released
        throw new Error("released context triggering change")

      stack = get_context_args( @context_stack, n )
      if n == null
        while @tags.length > 0
          tag = @tags.pop()
          tag.release()
      else

        if @tags.length == 0
          for child in @children
            tags = child.write( @parent_dom, stack... )
            @tags = @tags.concat( tags )
        else
          for tag in @tags
            tag.context( stack... )
    )
    @context( @context_stack... )

  @is_released = false
  release : () ->
    @is_released = true
    @expr_arg.release()
    delete @expr_arg

    for k, tag of @tags
      tag.release()
    delete @tags

  context : (context_stack...) =>
    if @is_released
      throw new Error("released context setting context")

    @context_stack = context_stack
    @expr_arg.SetContext( context_stack... )

class ListDeclaration
  constructor : (@expr, @children... ) ->

  append : (children...) ->
    for child in children
      @children.push( child )

  write : ( parent_dom, context_stack... ) =>
    return new List( @expr, parent_dom, @children, context_stack, @key_mapper )

  key_map : (func) =>
    @key_mapper = func
    return this

class List
  constructor : ( expr, parent_dom, children, context_stack, key_map ) ->
    @parent_dom = parent_dom
    @context_stack = context_stack
    @tags = {}
    @children = children
    @key_mapper = key_map

    @expr_arg = new ExprArgument( expr ).setter( (n,o) =>
      if @is_released
        throw new Error("released list responding to change")

      if n is null then n = {}
      mapped_n = {}
      for k, v of n
        if @key_mapper != undefined
          k = @key_mapper( v )
          if k instanceof Notify
            k = k.id
        mapped_n[ k ] = v

      for k, v of o
        if @key_mapper != undefined
          k = @key_mapper( v )
          if k instanceof Notify
            k = k.id

        if k not of mapped_n
          list = @tags[k]
          for tag in list
            tag.release()
          delete @tags[k]

      for k, v of n
        if @key_mapper != undefined
          k = @key_mapper( v )
          if k instanceof Notify
            k = k.id

        stack = get_context_args( @context_stack, v )
        if k not of @tags
          list = []
          for child in @children
            tags = child.write( @parent_dom, stack... )
            list = list.concat( tags )
          @tags[k] = list
        else
          # if a key exists, then reassign context.
          for tag in @tags[k]
            tag.context( stack... )
    )
    @context( @context_stack... )


  is_released : false
  release : () =>
    @is_released = true
    @expr_arg.release()

    for k, list of @tags
      for tag in list
        tag.release()
    delete @tags
    delete @expr_arg

  context : (context_stack...) =>
    if @is_released
      throw new Error("setting context for released element")
    @context_stack = context_stack
    @expr_arg.SetContext( context_stack... )

o = _CORE_ =
  span : (children...) -> new TagDeclaration( "span", children...)
  div : (children...) -> new TagDeclaration( "div", children...)
  li : (children...) -> new TagDeclaration( "li", children...)
  ul : (children...) -> new TagDeclaration( "ul", children...)
  ol : (children...) -> new TagDeclaration( "ol", children...)
  img : (children...) -> new TagDeclaration( "img", children...)
  a : (children...) -> new TagDeclaration( "a", children...)
  p : (children...) -> new TagDeclaration( "p", children...)
  h1 : (children...) -> new TagDeclaration( "h1", children...)
  h2 : (children...) -> new TagDeclaration( "h2", children...)
  h3 : (children...) -> new TagDeclaration( "h3", children...)
  h4 : (children...) -> new TagDeclaration( "h4", children...)
  h5 : (children...) -> new TagDeclaration( "h5", children...)
  h6 : (children...) -> new TagDeclaration( "h6", children...)
  tag: (tag, children...) -> new TagDeclaration( tag, children...)
  pre :(children...) -> new TagDeclaration("pre", children...)
  code :(children...) -> new TagDeclaration("code", children...)
  List :(expr, children...) -> new ListDeclaration( expr, children...)
  Context : (expr, children...) -> return new ContextDeclaration( expr, children...)
  _NativeEvent_ : null
  NativeEvent : () -> o._NativeEvent_

window.o = o
window.Notify = Notify
window.NotifyArray = NotifyArray